//
//  server.microhttpd.h
//  diylane-api
//
//  Created by Yucheng on 5/12/17.
//  Copyright © 2017 Yucheng. All rights reserved.
//

#ifndef __modules_server_h
#define __modules_server_h

#ifdef _MSC_VER
#pragma comment(lib, "libmicrohttpd_d.lib")
#endif

#ifdef __APPLE__
#pragma clang diagnostic push
#pragma clang system_header
#endif
#include <microhttpd.h>
#ifdef __APPLE__
#pragma clang diagnostic pop
#endif

#include <string>
#include <unordered_map>
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>

#include "jobs.hpp"
#include "cookies.hpp"

namespace diylane
{
namespace http
{

struct HttpServerConfig
{
    uint16_t port = 0;
    std::string addr = "0.0.0.0";
    std::string signature = "Diylane/1.0 - Powered by microhttpd";
};

struct HttpServer;

struct HttpRequest
{
    HttpServer *__server;
    struct MHD_Connection *__connection;
    const char *__url = NULL;
    const char *__method = NULL;
    const char *__version = NULL;
    const char *__upload_data = NULL;
    size_t __upload_data_size;

    // Used for middlewares
    typedef std::unordered_map<std::string, void *> Context;
    Context contexts;

    HttpRequest(HttpServer *server, struct MHD_Connection *conn)
    {
        __server = server;
        __connection = conn;
        __upload_data_size = 0;
    }

    ~HttpRequest()
    {
    }

    void setContext(const char *name, void *data)
    {
        contexts[name] = data;
    }

    void *getContext(const char *name)
    {
        Context::iterator itr = contexts.find(name);
        if (itr != contexts.end())
        {
            return itr->second;
        }
        return NULL;
    }

    void set_url(const char *url)
    {
        __url = url;
    }

    void set_method(const char *method)
    {
        __method = method;
    }

    void set_version(const char *version)
    {
        __version = version;
    }

    void set_upload_data(const char *data)
    {
        __upload_data = data;
    }

    void set_upload_data_size(size_t size)
    {
        __upload_data_size = size;
    }

    const char *getHeader(const char *name)
    {
        return MHD_lookup_connection_value(__connection, MHD_HEADER_KIND, name);
    }

    const char *getCookie(const char *name)
    {
        return MHD_lookup_connection_value(__connection, MHD_COOKIE_KIND, name);
    }

    const char *getParameter(const char *name)
    {
        return MHD_lookup_connection_value(__connection, MHD_GET_ARGUMENT_KIND, name);
    }
};

struct HttpResponse
{
    typedef std::unordered_map<std::string, std::string> Header;

    unsigned int status = MHD_HTTP_OK;
    std::string content_type = "text/plain";
    std::string charset = "utf-8";
    std::string content = "";

    HttpRequest *request;
    Header headers;
    http::CookieJar cookieJar;

    HttpResponse(HttpRequest *_request)
    {
        request = _request;
    }

    ~HttpResponse()
    {
    }

    void addHeader(const char *name, const char *value)
    {
        headers[name] = value;
    }

    void addHeader(std::string name, std::string value)
    {
        headers[name] = value;
    }

    std::string contentType()
    {
        return boost::str(boost::format("%s; charset=%s") % content_type % charset);
    }
};

typedef void (*HttpHandler)(HttpRequest *, HttpResponse *);
struct HttpRoute
{
    std::string uri;
    HttpHandler handler;
};

struct HttpMiddleware
{
    HttpMiddleware() {}
    virtual ~HttpMiddleware() {}
    virtual bool before(HttpRequest *request, HttpResponse *response) = 0;
    virtual void after(HttpRequest *request, HttpResponse *response) = 0;
};

struct HttpServer
{
    using self_t = HttpServer;

    HttpServerConfig config;
    std::vector<HttpRoute> routes;
    jobs::Pool jobPool;
    std::vector<HttpMiddleware *> middlewares;
    bool shutdown = false;
    struct MHD_Daemon *daemon = NULL;

    HttpServer()
    {
    }

    ~HttpServer()
    {
        if (daemon)
            MHD_stop_daemon(daemon);
    }

    self_t &addMiddleware(HttpMiddleware *middleware)
    {
        middlewares.push_back(middleware);
        return *this;
    }

    self_t &port(int n)
    {
        config.port = (uint16_t)n;
        return *this;
    }

    self_t &route(std::string uri, HttpHandler handler)
    {
        routes.push_back(HttpRoute{uri, handler});
        return *this;
    }

    void handleRequest(HttpRequest *request, HttpResponse *response)
    {
        HttpRoute *route = NULL;

        // find route
        for (size_t i = 0; i < routes.size(); i++)
        {
            HttpRoute *it = &routes[i];
            if (strcmp(it->uri.c_str(), request->__url) == 0)
            {
                route = it;
                break;
            }
        }

        // if not found
        if (route == NULL)
        {
            response->status = MHD_HTTP_NOT_FOUND;
            return;
        }

        // before middlewares
        for (size_t i = 0; i < middlewares.size(); i++)
        {
            HttpMiddleware *m = middlewares[i];
            if (m)
            {
                if (!m->before(request, response))
                {
                    //TODO status should be set by middlewares.
                    //response->status = MHD_HTTP_INTERNAL_SERVER_ERROR;
                    return;
                }
            }
        }

        // call the handler
        route->handler(request, response);

        // after middlewares
        for (size_t i = middlewares.size(); i > 0; i--)
        {
            HttpMiddleware *m = middlewares[i - 1];
            if (m)
            {
                m->after(request, response);
            }
        }
    }

    int run_answer_to_connection(
        struct MHD_Connection *connection,
        const char *url,
        const char *method,
        const char *version,
        const char *upload_data,
        size_t *upload_data_size,
        void **con_cls)
    {
        HttpRequest request(this, connection);
        request.set_url(url);
        request.set_method(method);
        request.set_version(version);
        request.set_upload_data(upload_data);
        request.set_upload_data_size(*upload_data_size);

        HttpResponse response(&request);

        handleRequest(&request, &response);

        struct MHD_Response *resp = MHD_create_response_from_buffer(response.content.length(), (void *)response.content.c_str(), MHD_RESPMEM_MUST_COPY);
        if (resp == NULL) {
            return MHD_NO;
        }

        // add missing headers
        response.headers[MHD_HTTP_HEADER_SERVER] = config.signature;
        response.headers[MHD_HTTP_HEADER_CONTENT_TYPE] = response.contentType();

        // add headers to response
        HttpResponse::Header::iterator iHeader;
        for (iHeader = response.headers.begin(); iHeader != response.headers.end(); iHeader++)
        {
            MHD_add_response_header(resp, iHeader->first.c_str(), iHeader->second.c_str());
        }

        // cookies
        http::Cookies::iterator iCookie;
        for (iCookie = response.cookieJar.cookies.begin(); iCookie != response.cookieJar.cookies.end(); iCookie++)
        {
            http::Cookie *cookie = iCookie->second;
            MHD_add_response_header(resp, MHD_HTTP_HEADER_SET_COOKIE, cookie->to_SetCookie().c_str());
        }

        int ret = MHD_queue_response(connection, response.status, resp);
        MHD_destroy_response(resp);
        *con_cls = NULL;
        return ret;
    }
    
    int run_answer_to_connection_foo(
                                 struct MHD_Connection *connection,
                                 const char *url,
                                 const char *method,
                                 const char *version,
                                 const char *upload_data,
                                 size_t *upload_data_size,
                                 void **con_cls)
    {
        struct MHD_Response *resp = MHD_create_response_from_buffer(3, (void *)"123", MHD_RESPMEM_PERSISTENT);
        int ret = MHD_queue_response(connection, 200, resp);
        MHD_destroy_response(resp);
        *con_cls = NULL;
        return ret;
    }

    static int answer_to_connection(
        void *cls,
        struct MHD_Connection *connection,
        const char *url,
        const char *method,
        const char *version,
        const char *upload_data,
        size_t *upload_data_size,
        void **con_cls)
    {
        self_t *This = (self_t *)cls;
        return This->run_answer_to_connection(connection, url, method, version, upload_data, upload_data_size, con_cls);
    }

    int run()
    {
        daemon = MHD_start_daemon(
            MHD_USE_THREAD_PER_CONNECTION,
            config.port,
            NULL,
            NULL,
            &answer_to_connection,
            this,
            MHD_OPTION_END);
        if (NULL == daemon)
            return 1;

        while (!shutdown)
        {
            boost::this_thread::sleep(boost::posix_time::milliseconds(100));
        }
        return 0;
    }
};
}
}

#endif
